Borland Online And The Cobb Group Present:


April, 1995 - Vol. 2 No. 4

Using Standard Template Library (STL) containers and iterators

In recent issues, we've discussed how container and iterator classes can simplify your C++ programs. Initially, we looked at creating iterator classes you could use with C-style arrays or your own containers ("An Introduction to Iterator Classes," July 1994).

Later, we compared this approach with that of using the container classes and iterator classes that are part of Borland's object-based container class library ("More on Iterator Classes," August 1994). Finally, we examined the Borland International Data Structures (BIDS) container and iterator classes and demonstrated the programming style they encourage ("Using the BIDS Iterator Classes," December 1994).

Unfortunately, each C++ compiler vendor seems to have created different, and often incompatible, designs for the container class libraries they ship. Since containers and iterators are common elements of non-trivial C++ programs, it was inevitable that the C++ programming community would search for a common solution that all C++ programmers could use.

As a result, the American National Standards Institute and International Standards Organization (ANSI/ISO) C++ committee decided to include the Standard Template Library (STL) as part of the forthcoming C++ standard. The STL, created by Alex Stepanov and Meng Lee of Hewlett-Packard, is a framework of template classes and template functions that provide most, if not all, of the container and iterator data structures you're likely to need for C++ programs.

In this article, we'll compare STL containers and iterators to BIDS containers and iterators you may have already used. To demonstrate how you can use STL classes and functions instead of the BIDS components, we'll rewrite the example program from the December issue using STL containers and iterators.

BIDS containers

First, let's briefly review the BIDS library. As you probably know, the BIDS library defines three concrete types of containers (types you'll use directly): collections, associations, and sequences. Typically, underneath these concrete BIDS classes are implementation classes, on which Borland bases concrete classes.

Implementation vs. concrete classes

To see how the implementation classes in the BIDS library relate to the concrete classes, consider the TArrayAsVector<> and TDVectorImp<> template classes. Figure A shows the relationships between the TArrayAsVector<> template class and its base classes, the relationship between the TDVectorImp<> template class and its bases, and the relationship of these two hierarchies to each other.


Figure A - The concrete classes and the implementation classes have a complex relationship.

Fortunately, you don't need to specify the interactions between these classes­­Borland has already defined the interactions and created several concrete classes for you. The implementation classes that the BIDS library uses represent the basic data structures that programmers have been using for years:

The BIDS concrete classes build on the data structures that we just identified to provide different interfaces to the same low-level implementation.

BIDS Collections

A BIDS Collection is basically an unordered set of objects. The two types of collections are Bag and Set. The only difference between a Bag and a Set is that a Set doesn't allow you to store multiple objects of the same value.

BIDS Associations

A BIDS Association is a set of paired elements. The two types of associations are Association and Dictionary. The Association class defines a relationship between a key element and a value element. The Dictionary class represents a set of associations.

BIDS Sequences

A BIDS Sequence is an ordered, dynamically sized set of objects you can access in very specific ways. The four Sequences are Queue, Deque, Stack, and Array. The Queue, Deque, and Stack classes restrict your access to one (or two, in the case of the Deque) element in the Set at a time. The Array class allows you to access any element in the Set via an integral index value.

STL containers

In comparison to the BIDS library, the STL seems somewhat sparse and simple. This is true for a couple of reasons. First, the BIDS classes contain a large number of intermediate classes to enhance flexibility, allowing you to combine the classes in several ways.

Second, the STL puts container manipulation code in global template functions that Borland included as member functions of specific BIDS classes. Later we'll compare these approaches.

STL Collections

An STL collection is an ordered container of objects. (This is in contrast to the collection classes in the BIDS library, where collections are unordered.) In the STL, you'll find two collections­­Set and Multiset. The Set class specifies an ordered collection for unique objects. The Multiset class allows duplicate objects and, therefore, corresponds roughly to the Bag class from the BIDS library.

STL Associations

In the BIDS library, you had to create an instance of the Association class (using a Key object and a Value object) before you could create a Dictionary of those associations. The STL bypasses this intermediate step by causing the Association classes to create instances of the Pair class (a utility class that defines paired values of possibly different types). The two association classes are Map and Multimap. The Map class corresponds directly to the BIDS Dictionary class­­it stores a collection of Pair objects and allows you to retrieve the value component of the Pair object based on a given key. The Multimap class provides this same behavior, but it allows duplicate keys for its Pair objects.

STL Sequences

As with the BIDS classes, STL sequence classes define ordered collections of objects you can access in specific ways. The three sequence classes are Vector, Deque, and List. You'll notice that the STL doesn't provide a separate implementation class for vectors and lists as the BIDS library does. Instead, it provides a more comprehensive interface that's used as a concrete class. As with their BIDS counterparts, an STL Vector allows you to access any element via an integral index, and an STL Deque allows you to add elements to or remove them from either end.

In addition, the STL provides a List class as a concrete class. The List class allows you to add elements to or remove them from the middle of the sequence more quickly than you could from a Vector or a Deque, but you have to traverse the list to access a specific element.

STL Adaptors

If you begin using the adaptor classes, the BIDS library and STL start to look very similar. In the STL, the Adaptor classes allow you to limit the functions that are accessible.

Specifically, the Adaptor classes allow you to modify the way the Sequence classes store or retrieve their elements. The three Adaptor classes are Stack, Queue, and Priority Queue. To implement a Stack, you can use any of the Sequence classes to provide the storage­­the Stack class simply translates "stack functions" into function calls that are appropriate for one of the Sequence classes. The Stack class merely provides a pop( ) member function that calls

c.pop_back( );

where c is an identifier for the adaptor's implementation sequence.

Float(ing) a container

To get a better idea of how you can use the STL classes in place of the BIDS classes, let's rewrite the example program from the December article. Before you start, you'll want to create a new directory for the STL header files and copy them into that directory. (If you don't already have the STL files, see How to get the STL, for information on acquiring the STL and its accompanying documentation.

To create the new directory, enter the following at a DOS command line:

md \BC4\INCLUDE\STL

Then, perform a simple DOS file copy to transfer all STL header files to that directory.

To create our example program, launch the Borland C++ 4.0 Integrated Development Environment (IDE). When the IDE's main window appears, choose New from the File menu and enter the program that appears in Listing A.


Listing A: STL_TEST.CPP

#include "\bc4\include\stl\vector.h"
#include "\bc4\include\stl\list.h"
#include "\bc4\include\stl\deque.h"

#include <iostream.h>

typedef vector<float> Group;
typedef Group::iterator Iterator;

int main( )
{
  // Create a container
  Group numbers;

  float num = 1.01;

  for(int count = 1;
      count < 6;
      ++count)
  {
    num = num * count;
    numbers.push_back(num);
    cout << num << endl;
  }

  cout << endl; // Add space

  while(!numbers.empty( ))
  {
    cout << numbers.back( ) << endl;
    numbers.pop_back( );
  }
 
 return 0;
}

When you finish entering the code, choose Save from the File menu and enter STL_TEST.CPP in the Save File As entry field. Click OK to save the file.

Next, right-click on the window for the code you just entered and choose Target Expert... from the pop-up menu. When the TargetExpert dialog box appears, choose EasyWin from the Target Type list box. Click OK when you're finished.

To make the STL files accessible, choose Project... from the Options menu. In the Project Options dialog box, click on Directories in the Topics list, and then add the path \BC4\INCLUDE\STL to the STL files at the end of the Include Source Directory entry field. Click OK to save this change.

Before you build any programs that use the STL, you'll need to make some minor changes for Borland C++ compatibility. (See Modifying the STL to work with Borland C++ 4.x, for more information.)

Now, choose Run from the Debug menu to build and run the program. When the program runs, you'll see the following output:

1.01
2.02
6.06
24.24
121.2
121.2 24.24 6.06 2.02 1.01

After you've confirmed that the output from the program is correct, double-click on the output window to close it.

If you examine this new version of the program, you'll notice several details of the STL itself. For example, instead of declaring an iterator type with another template, you'll notice that the STL defines the iterator (Group::iterator) as a nested type of the container class. Also, the push_back( ), empty( ), back( ), and pop_back( ) member functions demonstrate some of the STL container classes' common interface.

Iterators

As you may have noticed, we designed STL_TEST to use iterators differently from how they were used in the December article. We did this so we could present the STL iterators separately.

BIDS iterators

In the BIDS library, most of the container classes have their own iterator classes. For example, to iterate a TArrayAsVector<> or BI_ArrayAsVector<> array, you'd use either a TArrayAsVectorIterator<> object or a BI_ArrayAsVectorIterator<> object.

Because the BIDS library contains an iterator template class for most of the container template classes, the library is quite large. Fortunately, Borland defined a fairly consistent interface in each of the iterator classes, which made it easy to use, despite its size. (We demonstrated this in the December article by changing the underlying storage from a Stack container to a Queue container. The iterator interface is the same for both container types.)

STL iterators

Similarly, the STL defines several types of iterator classes that have a very consistent interface within each type. The iterator types are: input, output, forward, bidirectional, and random access.

For example, each of the three sequence classes uses a specific type of iterator object. Table A lists the sequence classes and their corresponding iterator types.

Table A: Sequence classes and their iterators
Sequence class Iterator
Vector random access
Deque random access with distance specifier
List bidirectional

Random access iterators

The random access iterator the Vector class uses is the simplest type­­a pointer to an object in the vector. This makes the Vector class an ideal component for wrapping around simple C++ arrays, because a pointer to any element in an array is a valid iterator for that type. (Since the Vector class uses simple, but managed, C++ arrays for its low-level storage, this makes perfect sense.)

The result of this design is that you can use pointers to array elements even if the array isn't wrapped inside the STL Vector class. For example, if you create an array of char values using

char str[20];

you can use str, &(str[0]), or &(str[5]) as random access iterators. This means you can call the find( ) template function by writing

find(str, &(str[20]), "t");

and the template function will correctly return a pointer to a "t" that appears in that array.

Random access iterators with distance specifier

While the Vector class uses simple pointers as random access iterators, the Deque class requires something to differentiate its iterators. This differentiation is necessary because elements may not have the same conceptual position in the deque that they have in memory. Because of the possible differences in conceptual position, moving "up" from a given position in the deque may cause the iterator to wrap around to the beginning of the deque's storage.

Bidirectional iterators

Unlike the random access iterators that the Vector and Deque classes use, the List class's iterators can move to another position only by "walking" the list (moving one list element at a time). This is because any given list element has access only to the elements immediately ahead of and behind it. This limitation makes it impossible to define a reasonable random access method for a List object.

A different perspective

When you're trying to understand what the STL iterator classes represent, it may be easier to think of these types of iterators in terms of their behavior, instead of considering them related classes (which they aren't). What each type of iterator shares with other iterators of the same type is the interface it provides.

When you think of iterators this way, the different type categories become a set of operations that an iterator must provide. These rules are enforced by the different function templates the STL supplies.

For example, the sort( ) template function expects a random access iterator, which defines the common mathematical relationships­­operator-( ), operator+( ), operator==( ), and so on. If you try to call the sort( ) function using iterators from a List, the compiler will generate Illegal Structure Operation errors for the lines of code that try to perform math operations on iterators that define only the pre-increment and pre-decrement operators.

However, since the random access iterators you can use with a Vector object provide the same operations that the Deque class's iterators provide, you can use the sort( ) function with iterators from either type of container. This is analogous to the global displayMatches( ) function that we used in the December article.

Finishing the example

Now, let's rewrite the displayMatches( ) function. As you may recall from the December article, this function had a very simple purpose: Search the container for values between 1 and 3 and display them via the standard output stream, cout. For reference, this function appears in Figure B.


Figure B - You need to change the displayMatches( ) function to make it compatible with the STL.

void
displayMatches(Iterator &i)
{
 i.restart( );
  while(i)
  {
    float temp = i.current( );
    if((temp > 1) && (temp < 3))
    {
      cout << "Item " << i.current( );
      cout << " is between 1 and 3" << endl;
    }
    ++i;
  }
} 

The approach

To write the displayMatches( ) function as an STL-compatible function, you need to make two changes. First, you must change the function's argument list to accept a second iterator that represents the last item in the container.

The BIDS iterator classes all provide an operator int( ) conversion operator that returns 0 if the iterator no longer points to a valid element in its container. Unfortunately, this design prevents us from using low-level constructs like simple pointers as iterators.

Instead, the STL algorithm functions accept two iterators that specify a range of elements. To give you a better idea of how the STL template functions work, we'll use this same approach.

Second, you must change this function into a template function. The old version relied on the

typedef BI_StackAsVectorIterator<float> Iterator;

statement to specify the iterator's static type. If you rewrite it as a template function, you'll be able to use it for more than one type of container.

Making changes

From the Borland C++ 4.0 IDE, reopen the STL_TEST.CPP file by double-clicking on its name in the Project window. When the editing window for this file appears, insert the following code directly above the main( ) function:

template <class ForwardIterator>
void
displayMatches(ForwardIterator start,
               ForwardIterator end)
{
  while(start != end)
  {
    if((*start > 1) && (*start < 3))
    {
      cout << "Item " << *start;
      cout << " is between 1 and 3" << endl;
    }
    ++start;
  }
}

Then, in the main( ) function, directly above the while(!numbers.empty( )) statement, insert the following commands:

displayMatches(numbers.begin( ), numbers.end( ));

cout << endl; 

Choose Save from the File menu to save these changes.

Next, choose Run from the Debug menu. After the compiler finishes building the application, it will run the application and display the output window. In this window, you'll see

1.01
2.02
6.06
24.24
121.2

Item 1.01 is between 1 and 3
Item 2.02 is between 1 and 3

121.2
24.24
6.06
2.02
1.01

When you've confirmed that the output is correct, double-click on the output window to close it.

Now, change the typedef statement for the Group type to

typedef deque<float> Group;

and rebuild and run the program. The output will be the same, which demonstrates that the displayMatches( ) template function is compatible with the sequence containers. (By the way, you can change the container to a list, and it will still work.)

Global template functions or class member functions?

By now, you may be wondering why the STL is such a big deal. After all, there are several classes that the BIDS library provides that don't appear to be part of the STL.

For example, the BIDS library defines the sorted array classes TSArrayAsVector<>, TMSArrayAsVector<>, TISArrayAsVector<>, and TMISArrayAsVector<>. To implement sorting in these classes, the BIDS library uses the implementation class TMSVectorImp<>, which defines an Add( ) member function (which, in turn, inserts the elements in the correct order) and a Find( ) member function (which locates specific elements).

Instead of relying on member functions to modify the contents of a container directly, the STL provides container classes that define a common interface, iterator classes that modify the class objects through that interface, and global template functions to manipulate the containers via those iterators. If you want to create a sorted vector of integers, you can create a global template function similar to

template<class Container, class T >
void sorted_insert(Container c, 
                  const T& value)
{ c.insert(lower_bound(c.begin( ), c.end( ), 
           value), value);
} 

This function uses one of the STL's comparison functions­­lower_bound( )­­to search for the appropriate location for the value object.

If you examine the sequence classes, notice that all three of them provide the form of the insert( ) function we use here. This means that the sorted_insert( ) template function works correctly for any of these types of containers.

In contrast, to add this type of functionality to the BIDS library, Borland created several new classes and had to modify the Add( ) member function in each class. Without question, writing and debugging one version of a function is easier than writing and debugging two, three, or more versions. This is how the STL can perform complex tasks with a relatively simple framework. (For more information on the general organization of the STL, see Standard C++ - Standard Template Library overview)

Conclusion

The STL is a flexible, powerful replacement for the BIDS library. In future versions of Borland C++, you may see the TurboVision library and ObjectWindows Library depend on STL classes and iterators instead of their BIDS counterparts.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.